﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Tycho.Utilities;
using System.Threading;
using Tycho.Runtime;

namespace Tycho.Transactions {
    public class TransactionalDynamicFrameObject : AnyObject {
        public override AnyObject OuterScope { get; set; }
        internal TransactionalFrame Frame { get; private set; }

        public TransactionalDynamicFrameObject (AnyObject outerScope) {
            OuterScope = outerScope;
            Frame = new TransactionalFrame ();
        }

        public override AnyObject InvokeMethod (AnyObject self, AnyObject name, params AnyObject [] arguments) {
            if (name == Symbols.RuntimeGetVariable) {
                return GetVariable (self, arguments [0], arguments [1].ExpectValue<int> ());
            } else if (name == Symbols.RuntimeSetVariable) {
                SetVariable (self, arguments [0], arguments [1].ExpectValue<int> (), arguments [2]);
                return arguments [2];
            } else {
                return RuntimeModule.DynamicStackFrame.InvokeMethod (self, name, arguments);
            }
        }

        public override AnyObject GetVariable (AnyObject self, AnyObject name, int frameIndex) {
            if (frameIndex == 0) {
                AnyObject var;
                if (Frame.TryGetValue (name, out var)) {
                    return var;
                } else {
                    throw TychoException.NoSuchVariable (name);
                }
            } else if (OuterScope != null) {
                return OuterScope.GetVariable (self, name, frameIndex - 1);
            } else {
                throw new TychoException ("stack frame parent missing for variable " + name);
            }
        }

        public override void SetVariable (AnyObject self, AnyObject name, int frameIndex, AnyObject value) {
            if (frameIndex == 0) {
                Frame [name] = value;
            } else if (OuterScope != null) {
                OuterScope.SetVariable (self, name, frameIndex - 1, value);
            } else {
                throw new TychoException ("stack frame parent missing for variable " + name);
            }
        }

        public bool HasVariable (AnyObject name) {
            AnyObject var;
            return Frame.TryGetValue (name, out var);
        }

        public override IEnumerable<AnyObject> ObjectReferences {
            get {
                HashSet<AnyObject> refs = new HashSet<AnyObject> ();
                foreach (var var in Frame.VariableNames) {
                    refs.Add (var);
                    refs.Add (Frame [var]);
                }

                return refs;
            }
        }

        public override AnyObject Variables {
            get {
                return RuntimeModule.CreateSet (Frame.VariableNames);
            }
        }

        public void Transactionalise () {
            Frame.Transactionalise ();
        }

        public override AnyObject this [AnyObject index] {
            get {
                return GetVariable (this, index, 0);
            }
            set {
                SetVariable (this, index, 0, value);
            }
        }
    }

    public class TransactionalFrame : ITransactionalObject {
        class FrameVariable {
            public AnyObject Object;
            public int Version;
            public bool IsModified;
            public bool IsNew;
            public bool IsAccessed;

            public FrameVariable (AnyObject obj) {
                Object = obj;
                Version = 1;
            }

            public FrameVariable (FrameVariable var) {
                Object = var.Object;
                Version = var.Version;
            }
        }

        Dictionary<AnyObject, FrameVariable> Variables;
        public long Version { get; private set; }

        public TransactionalFrame () {
            Variables = new Dictionary<AnyObject, FrameVariable> ();
            Version = 0;
        }

        #region ITransactionalObject Members

        public TransactionLog.LocalObject CreateTransactionLocalObject () {
            return CreateTransactionLocalObject (Variables);
        }

        public TransactionLog.LocalObject CreateNestedTransactionLocalObject (TransactionLog.LocalObject outerLocal) {
            return CreateTransactionLocalObject ((Dictionary<AnyObject, FrameVariable>) outerLocal.Object);
        }

        private TransactionLog.LocalObject CreateTransactionLocalObject (Dictionary<AnyObject, FrameVariable> variables) {
            Dictionary<AnyObject, FrameVariable> localVars = new Dictionary<AnyObject, FrameVariable> ();

            foreach (KeyValuePair<AnyObject, FrameVariable> var in variables) {
                localVars.Add (var.Key, new FrameVariable (var.Value));
            }

            return new TransactionLog.LocalObject (localVars, 0);
        }

        public void Commit () {
            bool isFrameModified = false;

            foreach (KeyValuePair<AnyObject, FrameVariable> localVar in TransactionLocalVariables) {
                if (localVar.Value.IsAccessed) {
                    FrameVariable liveVar;

                    if (Variables.TryGetValue (localVar.Key, out liveVar)) {
                        if (localVar.Value.IsModified) {
                            liveVar.Object = localVar.Value.Object;
                            liveVar.Version++;

                            isFrameModified = true;
                        }
                    } else if (localVar.Value.IsNew) {
                        Variables.Add (localVar.Key, new FrameVariable (localVar.Value.Object));
                        isFrameModified = true;
                    }
                }
            }

            if (isFrameModified) {
                Version++;
            }
        }

        public void RollBack () {
            // don't write changes back to live object - do nothing
        }

        public bool Prepare () {
            var localTransactionObject = TransactionalObjectManager.Manager.TransactionLog.GetLocalObject (this, false);
            var localVariables = (Dictionary<AnyObject, FrameVariable>) localTransactionObject.Object;

            foreach (KeyValuePair<AnyObject, FrameVariable> localVar in localVariables) {
                FrameVariable liveVar;

                if (Variables.TryGetValue (localVar.Key, out liveVar)) {
                    if ((localVar.Value.IsAccessed && localVar.Value.Version != liveVar.Version) || localVar.Value.IsNew) {
                        // variable has been accessed in this transaction but changed by another, or
                        // is new in this transaction and also new in another.
                        return false;
                    } else if (localVar.Value.IsModified) {
                        localTransactionObject.Modified = true;
                    }
                } else if (localVar.Value.IsNew) {
                    // new variable in transaction, fine
                    localTransactionObject.Modified = true;
                } else {
                    // variable deleted since transaction started
                    return false;
                }
            }

            return true;
        }

        public IEnumerable<AnyObject> ObjectReferences {
            get {
                HashSet<AnyObject> refs = new HashSet<AnyObject> ();
                foreach (FrameVariable var in TransactionLocalVariables.Values) {
                    refs.Add (var.Object);
                }
                return refs;
            }
        }

        Dictionary<AnyObject, FrameVariable> TransactionLocalVariables {
            get {
                if (IsTransactional) {
                    return (Dictionary<AnyObject, FrameVariable>) TransactionalObjectManager.Manager.TransactionLog.GetObject (this, false);
                } else {
                    return Variables;
                }
            }
        }

        public bool IsTransactional { get; private set; }

        public void Transactionalise () {
            IsTransactional = true;
        }

        #endregion

        #region IFrame<TKey,TValue> Members

        public AnyObject this [AnyObject index] {
            get {
                FrameVariable var = TransactionLocalVariables [index];
                var.IsAccessed = true;
                return var.Object;
            }
            set {
                Dictionary<AnyObject, FrameVariable> vars = TransactionLocalVariables;
                FrameVariable var;

                if (vars.TryGetValue (index, out var)) {
                    var.IsModified = true;
                    var.IsAccessed = true;
                    var.Object = value;
                } else {
                    var = new FrameVariable (value);
                    var.IsAccessed = true;
                    var.IsModified = true;
                    var.IsNew = true;
                    var.Object = value;
                    vars.Add (index, var);
                }
            }
        }

        public bool TryGetValue (AnyObject index, out AnyObject value) {
            FrameVariable var;
            if (TransactionLocalVariables.TryGetValue (index, out var)) {
                var.IsAccessed = true;
                value = var.Object;
                return true;
            } else {
                value = null;
                return false;
            }
        }

        public IEnumerable<AnyObject> VariableNames {
            get {
                return TransactionLocalVariables.Keys;
            }
        }

        #endregion
    }
}
